/*
  M5Paper老黄历项目

  在 M5Paper 的电子墨水屏上，呈现老黄历界面和信息

  作者：HonestQiao
  日期：2024-01-10
  版本：v1.0
*/

#include "M5EPD.h"
#include "FS.h"

// #include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#include "hV_Utilities_Common.h"

// 指示灯定义
// #define LED_BUILTIN 18

// 字体定义
// #define TTF_FONT_PATH "/fonts/GenSenRounded-R.ttf"
#define TTF_FONT_PATH "/fonts/OPPOSans-M.ttf"

// 黄历数据调用接口
// #define API_URL "http://192.168.1.15:8889/laohuangli.json"
#define API_URL "http://192.168.1.15:8889/api/v1.0/laohuangli/index/%s/%d"

// 实例化画板
M5EPD_Canvas canvas1(&M5.EPD);

// Define structures and classes
// 字体缓冲
const uint32_t render_sizes[] = { 16, 24, 48, 64 };

// 颜色定义
struct EPD_Colours {
  uint8_t black = 15;
  uint8_t darkRed = 12;
  uint8_t red = 10;
  uint8_t lightRed = 8;
  uint8_t grey = 5;
  uint8_t white = 0;
};

// 屏幕操作类
class EPD_Screen {
public:
  EPD_Screen();
  ~EPD_Screen();
  void clear();
  void flush();
  void setOrientation(uint16_t orientation);
  void setScale(float val);
  void setPenSolid(bool val);
  void setFontSolid(bool val);
  void selectFont(uint8_t val);
  void setTextSize(uint8_t val);
  void setTextDatum(uint8_t val);
  void gText(uint32_t x, uint32_t y, const String &string, uint32_t color);
  void point(uint32_t x, uint32_t y, uint32_t color);
  void line(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t color);
  void rectangle(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t color);
  void circle(uint32_t x, uint32_t y, uint32_t r, uint32_t color);
  void triangle(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2, uint32_t color);

  void dLine(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color);
  void dRectangle(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color);

private:
  bool _bPenSolid = true;
  float _scale = 1;

  uint32_t _s(uint32_t val) {
    return uint32_t((float)val * _scale);
  }
};

EPD_Screen::EPD_Screen() {
  _bPenSolid = true;
}

EPD_Screen::~EPD_Screen() {
}

void EPD_Screen::clear() {
  canvas1.deleteCanvas();

  canvas1.createCanvas(540, 960);
  for (int i = 0; i < sizeof(render_sizes) / sizeof(render_sizes[0]); i++) {

    if (!canvas1.isRenderExist(render_sizes[i])) {
      canvas1.createRender(render_sizes[i], 256);
    }
  }

  canvas1.fillCanvas(0);
}

void EPD_Screen::flush() {
  canvas1.pushCanvas(0, 0, UPDATE_MODE_GC16);
}

void EPD_Screen::setOrientation(uint16_t orientation) {
  M5.TP.SetRotation(orientation);
  M5.EPD.SetRotation(orientation);
}

void EPD_Screen::setScale(float val) {
  _scale = val;
}

void EPD_Screen::setPenSolid(bool val) {
  _bPenSolid = val;
}

void EPD_Screen::setFontSolid(bool val) {
}

void EPD_Screen::selectFont(uint8_t val) {
}

void EPD_Screen::setTextSize(uint8_t val) {
  canvas1.setTextSize(val);
}

void EPD_Screen::setTextDatum(uint8_t val) {
  canvas1.setTextDatum(val);
}

void EPD_Screen::gText(uint32_t x, uint32_t y, const String &string, uint32_t color) {
  canvas1.setTextColor(color);
  canvas1.drawString(string, _s(x), _s(y));
}

void EPD_Screen::point(uint32_t x, uint32_t y, uint32_t color) {
  canvas1.drawPixel(_s(x), _s(y), color);
}

void EPD_Screen::line(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t color) {
  canvas1.drawLine(_s(x), _s(y), _s(x1), _s(y1), color);
}

void EPD_Screen::rectangle(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t color) {
  if (_bPenSolid) {
    canvas1.fillRect(_s(x), _s(y), _s(x1 - x), _s(y1 - y), color);
  } else {
    canvas1.drawRect(_s(x), _s(y), _s(x1 - x), _s(y1 - y), color);
  }
}

void EPD_Screen::circle(uint32_t x, uint32_t y, uint32_t r, uint32_t color) {
  if (_bPenSolid) {
    canvas1.fillCircle(_s(x), _s(y), _s(r), color);
  } else {
    canvas1.drawCircle(_s(x), _s(y), _s(r), color);
  }
}

void EPD_Screen::triangle(uint32_t x, uint32_t y, uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2, uint32_t color) {
  if (_bPenSolid) {
    canvas1.fillTriangle(_s(x), _s(y), _s(x1), _s(y1), _s(x2), _s(y2), color);
  } else {
    canvas1.drawTriangle(_s(x), _s(y), _s(x1), _s(y1), _s(x2), _s(y2), color);
  }
}

void EPD_Screen::dLine(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color) {
  canvas1.drawLine(_s(x), _s(y), _s(x + w), _s(y + h), color);
}

void EPD_Screen::dRectangle(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color) {
  if (_bPenSolid) {
    canvas1.fillRect(_s(x), _s(y), _s(w), _s(h), color);
  } else {
    canvas1.drawRect(_s(x), _s(y), _s(w), _s(h), color);
  }
}

// Define variables and constants
EPD_Colours myColours1;
EPD_Screen myScreen;

uint32_t x_offset = 0;
uint32_t y_offset = 0;
uint16_t orientation = 90;

// 提示信息定义
uint32_t tips_x = (int)(2.2 * 50);
uint32_t tips_y = (int)(2.2 * 360);
String tips_text = "天灵灵，地灵灵，菩萨显灵";
String tips_connect = "稍等片刻，老黄历马上就到";
String tips_404 = "赶紧回头，天机%s";

// LED状态定义
#ifdef LED_BUILTIN
bool status = false;
#endif

// 天数定义
int days_index = 0;

// Prototypes

// Utilities
///
/// @brief Wait with countdown
/// @param second duration, s
///
void wait(uint8_t second) {
  for (uint8_t i = second; i > 0; i--) {
    Serial.print(formatString(" > %i  \r", i));
    delay(1000);
  }
  Serial.print("         \r");
}

// WiFi连接定义
#ifndef STASSID
#define STASSID "********"
#define STAPSK "********"
#endif

const char *ssid = STASSID;
const char *pass = STAPSK;

WiFiMulti WiFiMulti;

// 字体转换
uint8_t get_font(String font_name) {
#if 0
  if (font_name == "6x8") {
    return Font_Terminal6x8;
  } else if (font_name == "8x12") {
    return Font_Terminal8x12;
  } else if (font_name == "8x16") {
    return Font_Terminal8x16;
  } else if (font_name == "12x16") {
    return Font_Terminal12x16;
  } else if (font_name == "16x24") {
    return Font_Terminal16x24;
  } else {
    Serial.println(formatString("font_name is invalid: %s", font_name));
    return Font_Terminal6x8;
  }
#else
  return 0;
#endif
}

// 颜色转换
uint16_t get_color(String color_name) {
  if (color_name == "black") {
    return myColours1.black;
  } else if (color_name == "white") {
    return myColours1.white;
  } else if (color_name == "grey") {
    return myColours1.grey;
  } else if (color_name == "red") {
    return myColours1.red;
  } else if (color_name == "lightRed") {
    return myColours1.lightRed;
  } else if (color_name == "darkRed") {
    return myColours1.darkRed;
  } else {
    Serial.println(formatString("color_name is invalid: %s", color_name));
    return myColours1.black;
  }
}

// 对齐转换
uint8_t get_datum(String datum_name) {
  if (datum_name == "TL" || datum_name == "TL_DATUM") {
    return TL_DATUM;
  } else if (datum_name == "TC" || datum_name == "TC_DATUM") {
    return TC_DATUM;
  } else if (datum_name == "TR" || datum_name == "TR_DATUM") {
    return TR_DATUM;
  } else if (datum_name == "CL" || datum_name == "CL_DATUM") {
    return CC_DATUM;
  } else if (datum_name == "CC" || datum_name == "CC_DATUM") {
    return CL_DATUM;
  } else if (datum_name == "CR" || datum_name == "CR_DATUM") {
    return CR_DATUM;
  } else if (datum_name == "BL" || datum_name == "BL_DATUM") {
    return BL_DATUM;
  } else if (datum_name == "BC" || datum_name == "BC_DATUM") {
    return BC_DATUM;
  } else if (datum_name == "BR" || datum_name == "BR_DATUM") {
    return BR_DATUM;
  } else {
    Serial.println(formatString("datum_name is invalid: %s", datum_name));
    return TL_DATUM;
  }
}

// 显示提示
void show_tips(String string) {
  Serial.println(formatString("show_tips: %s", string.c_str()));
  myScreen.setOrientation(orientation);
  myScreen.setTextSize(24);
  myScreen.gText(tips_x, tips_y, string, 10);
}

// 请求老黄历json数据并进行显示
void request_json_and_display() {
  // wait for WiFi connection
  if ((WiFiMulti.run() == WL_CONNECTED)) {

    HTTPClient http;

    Serial.print("[HTTP] begin...\n");
    if (http.begin(formatString(API_URL, days_index < 0 ? "prev" : "next", abs(days_index)))) {  // HTTP
      Serial.print("[HTTP] GET...\n");
      // start connection and send HTTP header
      int httpCode = http.GET();

      // httpCode will be negative on error
      if (httpCode > 0) {
        // HTTP header has been send and Server response header has been handled
        Serial.printf("[HTTP] GET... code: %d\n", httpCode);

        // file found at server
        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          String payload = http.getString();
          Serial.println(payload);

          // Allocate the JSON document
          JsonDocument doc;

          // Parse JSON object
          DeserializationError error = deserializeJson(doc, payload);
          if (error) {
            Serial.print(F("deserializeJson() failed: "));
            Serial.println(error.f_str());
            show_tips( String(formatString(tips_404.c_str(), "不可解析也")));
            return;
          } else {
            // 解析数据
            x_offset = doc["offset"][0].as<long>();
            y_offset = doc["offset"][1].as<long>();
            orientation = doc["orientation"].as<long>();

            tips_x = doc["tips"]["x"].as<long>();
            tips_y = doc["tips"]["y"].as<long>();
            tips_text = doc["tips"]["text"].as<const char *>();

            Serial.println(F("Response:"));
            Serial.println(formatString("name: %s", doc["name"].as<const char *>()));
            Serial.println(formatString("orientation: %d", orientation));
            Serial.println(formatString("offset: (%d,%d)", x_offset, y_offset));
            Serial.println(formatString("version: %0.2f", doc["version"].as<float>()));
            Serial.println(formatString("data size: %d", doc["data"].size()));

            // 设置默认方向
            myScreen.setOrientation(orientation);

            for (int i = 0; i < doc["data"].size(); i++) {
              String type = doc["data"][i]["type"];
              if (type == "comment") {
                // 注释
                String comment = doc["data"][i]["value"];
                Serial.println(formatString("comment: %s", comment.c_str()));
              } else if (type == "setOrientation") {
                // 设置旋转方向
                int value = doc["data"][i]["value"].as<long>();
                Serial.println(formatString("setOrientation: %d", value));
                myScreen.setOrientation(value);
              } else if (type == "setOffset") {
                // 设置坐标偏移
                x_offset = doc["data"][i]["x"].as<long>();
                y_offset = doc["data"][i]["y"].as<long>();
                Serial.println(formatString("setOffset: (x, y)=(%d, %d)", x_offset, y_offset));
              } else if (type == "setPenSolid") {
                // 设置落笔抬笔
                int value = doc["data"][i]["value"].as<bool>();
                Serial.println(formatString("setPenSolid: %d", value));
                myScreen.setPenSolid(value);
              } else if (type == "selectFont") {
                // 设置字体
                String font_name = doc["data"][i]["value"];
                Serial.println(formatString("selectFont: %s", font_name.c_str()));
                uint8_t font = get_font(font_name);
                myScreen.selectFont(font);
              } else if (type == "setTextSize") {
                // 设置字体大小
                int size = doc["data"][i]["value"].as<long>();
                Serial.println(formatString("setTextSize: %d", size));
                myScreen.setTextSize(size);
              } else if (type == "setScale") {
                // 设置坐标缩放
                float scale = doc["data"][i]["value"].as<float>();
                Serial.println(formatString("setScale: %0.2f", scale));
                myScreen.setScale(scale);
              } else if (type == "setTextDatum") {
                // 设置对齐方向
                String datum_name = doc["data"][i]["value"];
                int datum = get_datum(datum_name);
                Serial.println(formatString("setTextDatum: %s:%d", datum_name, datum));
                myScreen.setTextDatum(datum);
              } else if (type == "setFontSolid") {
                // 设置字体落笔抬笔
                int value = doc["data"][i]["value"].as<bool>();
                Serial.println(formatString("setFontSolid: %d", value));
                myScreen.setFontSolid(value);
              } else if (type == "gText") {
                // 显示文本
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                String text = doc["data"][i]["text"];
                String color_name = doc["data"][i]["color"];
                uint16_t color = get_color(color_name);
                Serial.println(formatString("gText: (x,y)=(%d,%d) text=%s color=%s:%d", x, y, text.c_str(), color_name.c_str(), color));
                myScreen.gText(x + x_offset, y + y_offset, text, color);
              } else if (type == "point") {
                // 画点
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("point: (x,y)=(%d,%d) color=%s", x, y, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.point(x + x_offset, y + y_offset, color);
              } else if (type == "line") {
                // 画线
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int x1 = doc["data"][i]["x1"].as<long>();
                int y1 = doc["data"][i]["y1"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("line: (x,y)=(%d,%d) (x1,y1)=(%d,%d) color=%s", x, y, x1, y1, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.line(x + x_offset, y + y_offset, x1 + x_offset, y1 + y_offset, color);
              } else if (type == "rectangle") {
                // 画矩形
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int x1 = doc["data"][i]["x1"].as<long>();
                int y1 = doc["data"][i]["y1"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("rectangle: (x,y)=(%d,%d) (x1,y1)=(%d,%d) color=%s", x, y, x1, y1, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.rectangle(x + x_offset, y + y_offset, x1 + x_offset, y1 + y_offset, color);
              } else if (type == "circle") {
                // 画圆
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int r = doc["data"][i]["r"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("circle: (x,y)=(%d,%d) r=%d color=%s", x, y, r, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.circle(x + x_offset, y + y_offset, r, color);
              } else if (type == "triangle") {
                // 画三角形
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int x1 = doc["data"][i]["x1"].as<long>();
                int y1 = doc["data"][i]["y1"].as<long>();
                int x2 = doc["data"][i]["x2"].as<long>();
                int y2 = doc["data"][i]["y2"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("triangle: (x,y)=(%d,%d) (x1,y1)=(%d,%d) (x2,y2)=(%d,%d) color=%s", x, y, x1, y1, x2, y2, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.triangle(x + x_offset, y + y_offset, x1 + x_offset, y1 + y_offset, x2 + x_offset, y2 + y_offset, color);
              } else if (type == "dLine") {
                // 画线(向量)
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int w = doc["data"][i]["w"].as<long>();
                int h = doc["data"][i]["h"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("dLine: (x,y)=(%d,%d) (w,h)=(%d,%d) color=%s", x, y, w, h, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.dLine(x + x_offset, y + y_offset, w, h, color);
              } else if (type == "dRectangle") {
                // 画矩形(向量)
                int x = doc["data"][i]["x"].as<long>();
                int y = doc["data"][i]["y"].as<long>();
                int w = doc["data"][i]["w"].as<long>();
                int h = doc["data"][i]["h"].as<long>();
                String color_name = doc["data"][i]["color"];
                Serial.println(formatString("dRectangle: (x,y)=(%d,%d) (w,h)=(%d,%d) color=%s", x, y, w, h, color_name.c_str()));
                uint16_t color = get_color(color_name);
                myScreen.dRectangle(x + x_offset, y + y_offset, w, h, color);
              } else {
                Serial.println(formatString("type: %s", type));
              }
            }

            Serial.println("数据解析完成，刷新屏幕");
          }
        } else {
          Serial.printf("[HTTP] GET... failed, code: %s\n", http.errorToString(httpCode).c_str());
          show_tips( String(formatString(tips_404.c_str(), "不可泄露也")));
        }
      } else {
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        show_tips( String(formatString(tips_404.c_str(), "不可妄得也")));
      }

      http.end();
    } else {
      Serial.println("[HTTP} Unable to connect");
      show_tips( String(formatString(tips_404.c_str(), "不可窥探也")));
    }
  }
}

void setup() {
  // M5初始化
  M5.begin();
  M5.TP.SetRotation(90);
  M5.EPD.SetRotation(90);
  M5.EPD.Clear(true);
  M5.RTC.begin();

  // 字体调用
  canvas1.loadFont(TTF_FONT_PATH, SD);  // Load font files from SD Card

#ifdef LED_BUILTIN
  pinMode(LED_BUILTIN, OUTPUT);
#endif
  // Serial.begin(115200);
  // Serial.setDebugOutput(true);

  Serial.println();
  Serial.println();
  Serial.println();

  // wait(1);

  // 显示启动提示
  myScreen.clear();
  show_tips(tips_connect);
  myScreen.flush();
  // delay(1000);

#ifdef LED_BUILTIN
  status = true;
  digitalWrite(LED_BUILTIN, status);
#endif

  WiFiMulti.addAP(ssid, pass);
  Serial.print("WiFi is connecting: ");
  while ((WiFiMulti.run() != WL_CONNECTED)) {
    Serial.print(".");
    Serial.flush();
    delay(100);

#ifdef LED_BUILTIN
    digitalWrite(LED_BUILTIN, status);
    status = !status;
#endif
  }
  Serial.println(" OK!");

#ifdef LED_BUILTIN
  status = false;
  digitalWrite(LED_BUILTIN, status);
#endif

  Serial.println("begin... ");
  // myScreen.begin();
  // Serial.println(formatString("%s %ix%i", myScreen.WhoAmI().c_str(), myScreen.screenSizeX(), myScreen.screenSizeY()));

  Serial.println("request json and display... ");

#ifdef LED_BUILTIN
  status = true;
  digitalWrite(LED_BUILTIN, status);
#endif

  // 显示加载提示
  myScreen.clear();
  show_tips(tips_text);
  myScreen.flush();

  // delay(1000);

  // 开机自动加载最新老黄历数据
  myScreen.clear();
  request_json_and_display();
  myScreen.flush();

#ifdef LED_BUILTIN
  status = false;
  digitalWrite(LED_BUILTIN, status);
#endif

  Serial.println("=== ");
  Serial.println();
}

void loop() {
  M5.update();
  // 刷新标志
  bool bFlush = false;
  // 按键检查
  if (M5.BtnP.wasPressed()) {
    // 中键，只刷新
    bFlush = true;
    Serial.printf("\a\aYou pressed BtnP!\n");
  }
  if (M5.BtnL.wasPressed()) {
    // 上键，前一天
    bFlush = true;
    days_index--;
    Serial.printf("\a\aYou pressed BtnL!\n");
  }
  if (M5.BtnR.wasPressed()) {
    // 下键，后一天
    bFlush = true;
    days_index++;
    Serial.printf("\a\aYou pressed BtnR!\n");
  }

  // 刷新数据
  if (bFlush) {
    Serial.printf("days_index is %d\n", days_index);

#ifdef LED_BUILTIN
    status = true;
    digitalWrite(LED_BUILTIN, status);
#endif

    // Serial.printf("\a\aYou pressed Btn*!\n");
    // while (M5.BtnR.isPressed()) {
    //     delay(1);
    // }

#ifdef LED_BUILTIN
    status = false;
    digitalWrite(LED_BUILTIN, status);
    delay(500);

    status = true;
    digitalWrite(LED_BUILTIN, status);
#endif

    // 显示加载提示
    myScreen.clear();
    show_tips(tips_text);
    myScreen.flush();
    // delay(1000);

    // 请求并显示老黄历数据
    myScreen.clear();
    request_json_and_display();
    myScreen.flush();
    Serial.println("刷新完成");

#ifdef LED_BUILTIN
    status = false;
    digitalWrite(LED_BUILTIN, status);
#endif
  }
  delay(100);
}
